﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using MegavaniaX.Actors.States;
using Microsoft.Xna.Framework;
using MegavaniaX.World;
namespace MegavaniaX
{
    /// <summary>
    /// Abstract class that involves all actors in the game world.
    /// </summary>
    public abstract class Actor : IRenderable, IEventDispatcher
    {
        /// <summary>
        /// terminal velocity
        /// </summary>
        public const float TerminalVelocityY = 0.40f;

        public StateMachine StateMachine
        {
            get;
            protected set;
        }

        public bool IsFacingLeft
        {
            get;
            set;
        }

        public bool IsAffectedByWalls
        {
            get;
            protected set;
        }

        /// <summary>
        /// whether or not this actor can/should be removed when it goes off the screen
        /// </summary>
        public bool IsRemovable
        {
            get;
            protected set;
        }

        /// <summary>
        /// Actor ID (some name that is a unique identifier).
        /// </summary>
        public String ActorID { get; set; }

        private Animation _currentAnimation;

        /// <summary>
        /// The current animation of this actor.
        /// </summary>
        public Animation CurrentAnimation
        {
            get
            {
                return _currentAnimation;
            }
            set
            {
                _currentAnimation = value;
                _currentAnimation.Reset();
            }
        }

        public float Bottom
        {
            get
            {
                return this.position.Y + Height;
            }
            set
            {
                position.Y = value - Height;
            }
        }

        public float Right
        {
            get
            {
                return this.position.X + Width;
            }
            set
            {
                this.position.X = value - Width;
            }
        }

        public float CenterX
        {
            get
            {
                return (this.position.X) + (Width / 2.0f);
            }
        }

        public float CenterY
        {
            get
            {
                return (this.position.Y) + (Height / 2.0f);
            }
        }

        public float Top
        {
            get
            {
                return this.position.Y;
            }
            set
            {
                this.position.Y = value;
            }
        }

        public float Left
        {
            get
            {
                return this.position.X;
            }
            set
            {
                this.position.X = value;
            }
        }

        /// <summary>
        /// Width of the object, for collision detection.
        /// </summary>
        public Int32 Width
        {
            get;
            set;
        }

        /// <summary>
        /// Height of the object, for collision detection.
        /// </summary>
        public Int32 Height
        {
            get;
            set;
        }


        /// <summary>
        /// The (x,y) position of this actor in the game world.
        /// </summary>
        public Vector2 position;

        /// <summary>
        /// The current (x,y) velocity of this actor.
        /// </summary>
        public Vector2 velocity;

        /// <summary>
        /// The current (x,y) acceleration of this actor.
        /// </summary>
        public Vector2 acceleration;

        #region Actor properties

        protected bool _IsAffectedByGravity = false;
        protected bool _IsAirborne = false;
        protected bool _IsDamagingToPlayer = false;

        public bool IsAffectedByGravity { get; protected set; }

        public bool IsAirborne { get; protected set; }
        public bool IsDamagingToPlayer { get; protected set; }
        public bool IsTakesDamage { get; protected set; }
        public bool IsDealsDamage { get; protected set; }
        public bool IsPlayer { get; protected set; }
        public bool IsHurtsPlayer { get; protected set; }
        public bool IsAIActor { get; protected set; }

        #endregion


        /// <summary>
        /// Default constructor.
        /// </summary>
        public Actor(String id)
        {
            ActorID = id;
            helper = new MessagingHelper();
            position = Vector2.Zero;
            velocity = Vector2.Zero;            
        }

        /// <summary>
        /// Gets the current sprite of this object.
        /// </summary>
        /// <returns></returns>
        public abstract MegavaniaSprite CurrentSprite();

        /// <summary>
        /// Updates the actor.
        /// </summary>
        /// <param name="elapsed">Time elapsed since the last update.</param>
        public virtual void Update(TimeSpan elapsed, Screen screen)
        {
            if (StateMachine != null)
                StateMachine.Update(elapsed, screen);

            /*
            MegavaniaX.logger.DebugFormat("Actor {0}: position ({1}, {2}) velocity ({3}, {4}) accel ({5}, {6})",
                ActorID,
                position.X, position.Y,
                velocity.X, velocity.Y,
                acceleration.X, acceleration.Y
                );
            */
            /*
            velocity.X += acceleration.X * elapsed.Milliseconds;

            velocity.Y += acceleration.Y * elapsed.Milliseconds;
            if (velocity.Y > TerminalVelocityY)
                velocity.Y = TerminalVelocityY;
            
            position.X = position.X + velocity.X * elapsed.Milliseconds;
            if (IsAffectedByWalls)
            {
                PushActorOutOfWalls(screen);
            }
            position.Y = position.Y + velocity.Y * elapsed.Milliseconds;
            if (IsAffectedByWalls)
            {
                PushActorOutOfCeilings(screen);
            }

            // gravity
            if (IsAffectedByGravity)
            {
                // fall checking
                bool onSolidGround = IsActorOnSolidGround(screen);
                if (!onSolidGround && !IsAirborne)
                {
                    StartFalling();
                }
                else if (onSolidGround && IsAirborne)
                {
                    StopFalling();
                }

            }
            */

            CurrentAnimation.Update(elapsed);
        }

        #region Gravity

        public virtual void StartFalling() { }
        public virtual void StopFalling() { }

        #endregion




        #region Screen Helper Methods

        internal void PushActorOutOfCeilings(Screen screen)
        {
            // first push out of floors
            var tileBottomLeft = screen.GetTileAt(this.Left + 1.0f, this.Bottom);
            var tileBottomRight = screen.GetTileAt(this.Right - 1.0f, this.Bottom);
            if (tileBottomLeft == null || tileBottomRight == null)
            {
                MegavaniaX.logger.Error(string.Format("Actor with center {0} and bottom {1} could not find appropriate tile!", this.CenterX, this.Bottom));
                return;
            }
            var tbrCoords = screen.GetTileCoords(tileBottomRight);
            if ((tileBottomLeft.IsSolid || tileBottomRight.IsSolid) && Bottom > tbrCoords.Y) // same gridY
            {
                MegavaniaX.logger.DebugFormat("Actor with Right {0} Bottom {1} Left {2} found to be stuck in tile {3}, {4}.  Pushing out to new bottom of {5}",
                    Right, Bottom, Left, tileBottomRight.X, tileBottomLeft.Y, tbrCoords.Y);
                Bottom = tbrCoords.Y;
            }


            // push out of ceilings
            var tileRight = screen.GetTileAt(this.CenterX + this.Width / 3.0f - 1.0f, this.Top - 1.0f);
            var tileCenter = screen.GetTileAt(this.CenterX, this.Top - 1.0f);
            var tileLeft = screen.GetTileAt(this.CenterX + this.Width / 3.0f + 1.0f, this.Top - 1.0f);
            if (tileRight == null || tileCenter == null || tileLeft == null)
            {
                MegavaniaX.logger.DebugFormat(string.Format("Actor with center {0} and bottom {1} could not find appropriate tile for ceiling check!  Pushing to {2}", CenterX, CenterY, screen.TopBound));
                this.Top = screen.TopBound;
                if (velocity.Y < 0.0f) // if rising, we want to set velocity = 0
                    velocity.Y = 0.0f;
                return;
            }

            if (tileRight.IsSolid || tileLeft.IsSolid || tileCenter.IsSolid)
            {
                var trCoords = screen.GetTileCoords(tileRight);
                // both tileright and tileLeft should have same gridY
                if (Top != (trCoords.Y + 16.0f + 1.0f))
                {
                    // nudge top directly
                    Top = (trCoords.Y + 16.0f + 1.0f);
                }
                if (velocity.Y < 0.0f) // if rising, we want to set velocity = 0
                    velocity.Y = 0.0f;
            }



        }

        internal bool IsActorOnSolidGround(Screen screen)
        {
            // translate actor's position into a tile on our grid.  get one pixel BELOW the actor's bottom
            var tileRight = screen.GetTileAt(this.Right - 1.0f, this.Bottom + 1.0f);
            var tileCenter = screen.GetTileAt(this.CenterX, this.Bottom + 1.0f);
            var tileLeft = screen.GetTileAt(this.Left + 1.0f, this.Bottom + 1.0f);
            if (tileRight == null || tileCenter == null || tileLeft == null)
            {
                MegavaniaX.logger.Error(string.Format("Actor with center {0} and bottom {1} could not find appropriate tile for ground checking!", CenterX, CenterY));
                return false;
            }

            if (tileRight.IsSolid || tileLeft.IsSolid || tileCenter.IsSolid)
            {
                var trCoords = screen.GetTileCoords(tileRight);
                // both tileright and tileLeft should have same gridY
                if (Bottom != (trCoords.Y))
                {
                    // nudge to be on bottom directly
                    Bottom = (trCoords.Y);
                }
                return true;
            }
            else
            {
                // MegavaniaX.logger.DebugFormat("Falling because tile ({0}, {1}) or ({2}, {3}) was found to be non-solid", tileRight.gridX, tileRight.gridY, tileLeft.gridX, tileLeft.gridY);
                return false;
            }

        }

        internal void PushActorOutOfWalls(Screen screen)
        {


            var tileRightA = screen.GetTileAt(this.Right + 1.0f, this.Bottom - 1.0f);
            var tileRightB = screen.GetTileAt(this.Right + 1.0f, this.Top + 1.0f);
            var tileLeftA = screen.GetTileAt(this.Left - 1.0f, this.Top + 1.0f);
            var tileLeftB = screen.GetTileAt(this.Left - 1.0f, this.Bottom - 1.0f);

            // error case: no tiles on either side
            if ((tileRightA == null || tileRightB == null) && (tileLeftA == null || tileLeftB == null))
            {
                MegavaniaX.logger.ErrorFormat("Actor with center {0} and bottom {1} had no left OR right tile (in the abyss).  Placing at {2}, {3}", this.CenterX, this.Bottom, screen.TopBound + Height * 2, screen.LeftBound + Width * 2);
                this.Left = screen.LeftBound + Width * 2;
                this.Top = screen.TopBound + Height * 2;
                return;
            }

            if (tileRightA == null || tileRightB == null)
            {
                MegavaniaX.logger.Debug(string.Format("Actor with center {0} and bottom {1} has no tile on right side.", this.CenterX, this.Bottom));
                this.Right = screen.RightBound;
                return;
            }
            if (tileLeftA == null || tileLeftB == null)
            {
                MegavaniaX.logger.Debug(string.Format("Actor with center {0} and bottom {1} has no tile on left side.", this.CenterX, this.Bottom));
                this.Left = screen.LeftBound;
                return;
            }

            if ((tileRightA.IsSolid || tileRightB.IsSolid) && this.Right != tileRightA.X * 16.0f)
            {
                var trCoords = screen.GetTileCoords(tileRightA);
                MegavaniaX.logger.DebugFormat("Actor with Right {0} Bottom {1} Left {2} found to be stuck in tile {3}, {4} on right side.  Pushing out to new right of {5}",
                    this.Right, this.Bottom, this.Left, tileRightA.X, tileRightA.Y, trCoords.X);
                this.Right = trCoords.X;
            }

            if ((tileLeftB.IsSolid || tileLeftA.IsSolid) && this.Left != ((tileLeftA.X * 16.0f) + 16.0f))
            {
                var tlCoords = screen.GetTileCoords(tileLeftA);
                MegavaniaX.logger.DebugFormat("Actor with Right {0} Bottom {1} Left {2} found to be stuck in tile {3}, {4} on left side.  Pushing out to new left of {5}",
                    this.Right, this.Bottom, this.Left, tileLeftA.X, tileLeftA.Y, tlCoords.X + 16.0f);
                this.Left = tlCoords.X + 16.0f;
            }

        }

        #endregion

        #region IEventDispatcher

        private MessagingHelper helper;

        /// <summary>
        /// Subscribes the given listener to events of type <paramref name="MegavaniaEventType"/>.
        /// </summary>
        /// <param name="callback">The callback that will be executed.</param>
        /// <param name="type">The type of event to listen to.</param>
        /// <param name="name">The name of the entity that we're listening to for this event.</param>
        public void Subscribe(MegavaniaEventCallback callback, MegavaniaEventType type, string name)
        {
            helper.Subscribe(callback, type, name);
        }

        /// <summary>
        /// Dispatches an event to any subscribed listeners.
        /// </summary>
        /// <param name="evt">The event to dispatch.</param>
        public void DispatchEvent(IMegavaniaEvent evt)
        {
            helper.DispatchEvent(evt);
        }


        #endregion

    }
}
